Creating a custom fluid using GenericPhase

OpenPNM comes with a small selection of pre-written phases (Air, Water, Mercury). In many cases users will want different options but it is not feasible or productive to include a wide variety of fluids. Consequntly OpenPNM has a mechanism for creating custom phases for this scneario. This requires that the user have correlations for the properties of interest, such as the viscosity as a function of temperature in the form of a polynomial for instance. This is process is described in the following tutuorial:

Import the usual packages and instantiate a small network for demonstration purposes:


In [1]:
import numpy as np
import openpnm as op

In [12]:
pn = op.network.Cubic(shape=[3, 3, 3], spacing=1e-4)
print(pn)


――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
openpnm.network.Cubic : net_01
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Properties                                    Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.coords                                      27 / 27   
2     throat.conns                                     54 / 54   
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Labels                                        Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.all                                      27        
2     pore.back                                     9         
3     pore.bottom                                   9         
4     pore.front                                    9         
5     pore.internal                                 27        
6     pore.left                                     9         
7     pore.right                                    9         
8     pore.surface                                  26        
9     pore.top                                      9         
10    throat.all                                    54        
11    throat.internal                               54        
12    throat.surface                                48        
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Now that a network is defined, we can create a GenericPhase object associated with it. For this demo we'll make an oil phase, so let's call it oil:


In [13]:
oil = op.phases.GenericPhase(network=pn)
print(oil)


――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
openpnm.phases.GenericPhase : phase_01
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Properties                                    Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.pressure                                    27 / 27   
2     pore.temperature                                 27 / 27   
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Labels                                        Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.all                                      27        
2     throat.all                                    54        
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

As can be seen in the above printout, this phase has a temperature and pressure set at all locations, but has no other physical properties.

There are 2 ways add physical properties. They can be hard-coded, or added as a 'pore-scale model'.

  • Some are suitable as hard coded values, such as molecular mass
  • Others should be added as a model, such as viscosity, which is a function of temperature so could vary spatially and should be updated depending on changing conditions in the simulation.

Start with hard-coding:


In [35]:
oil['pore.molecular_mass'] = 100.0  # g/mol

In [36]:
print(oil['pore.molecular_mass'])


[100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.
 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.]

As can be seen, this puts the value of 100.0 g/mol in every pore. Note that you could also assign each pore explicitly with a numpy array. OpenPNM automatically assigns a scalar value to every location as shown above.


In [37]:
oil['pore.molecular_mass'] = np.ones(shape=[pn.Np, ])*120.0

In [38]:
print(oil['pore.molecular_mass'])


[120. 120. 120. 120. 120. 120. 120. 120. 120. 120. 120. 120. 120. 120.
 120. 120. 120. 120. 120. 120. 120. 120. 120. 120. 120. 120. 120.]

You can also specify something like viscosity this way as well, but it's not recommended:


In [39]:
oil['pore.viscosity'] = 1600.0  # cP

The problem with specifying the viscosity as a hard-coded value is that viscosity is a function of temperature (among other things), so if we adjust the temperature on the oil object it will have no effect on the hard-coded viscosity:


In [40]:
oil['pore.temperature'] = 100.0  # C
print(oil['pore.viscosity'])


[1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600.
 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600. 1600.
 1600. 1600. 1600.]

The correct way to specify something like viscosity is to use pore-scale models. There is a large libary of pre-written models in the openpnm.models submodule. For instance, a polynomial can be used as follows:

$$ viscosity = a_0 + a_1 \cdot T + a_2 \cdot T^2 = 1600 + 12 T - 0.05 T^2$$

In [29]:
mod = op.models.misc.polynomial
oil.add_model(propname='pore.viscosity', model=mod, 
            a=[1600, 12, -0.05], prop='pore.temperature')

We can now see that our previously written values of viscosity (1600.0) have been overwritten by the values coming from the model:


In [30]:
print(oil['pore.viscosity'])


[2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300.
 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300. 2300.
 2300. 2300. 2300.]

And moreover, if we change the temperature the model will update the viscosity values:


In [31]:
oil['pore.temperature'] = 40.0  # C
oil.regenerate_models()
print(oil['pore.viscosity'])


[2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000.
 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000.
 2000. 2000. 2000.]

Note the call to regenerate_models, which is necessary to actually re-run the model using the new temperature.

When a pore-scale model is added to an object, it is stored under the models attribute, which is a dictionary with names corresponding the property that is being calculated (i.e. 'pore.viscosity'):


In [33]:
print(oil.models)


―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#   Property Name                       Parameter                 Value
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1   pore.viscosity                      model:                    polynomial
                                        a:                        [1600, 12, -0.05]
                                        prop:                     pore.temperature
                                        regeneration mode:        normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

We can reach into this dictionary and alter the parameters of the model if necessary:


In [34]:
oil.models['pore.viscosity']['a'] = [1200, 10, -0.02]
oil.regenerate_models()
print(oil['pore.viscosity'])


[1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568.
 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568. 1568.
 1568. 1568. 1568.]

The models submodule has a variety of common functions, stored under models.misc.basic_math or models.misc.common_funtions. There are also some models specific to physical properties under models.phases.